iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Engineering

Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability系列 第 35 篇

Day 22(上)|Golden Signals、RED、USE:框架是起點,不是儀表板模板

  • 分享至 

  • xImage
  •  

GitHub:darkstar1227/learning-sre-for-ai-era

結論先說:Golden Signals、RED、USE 是選問題的框架,不是要把所有圖表塞進一頁;選哪個要看你正在觀察的是使用者體驗、API,還是資源。本篇(上)先講清楚三套框架各自的定位、AI 時代多出來的訊號,以及怎麼把選好的框架落成一份可版控的指標契約;下篇(下)接著做 SRE Lab 實作與事故情境驗證。

Golden Signals 看 latency、traffic、errors、saturation;RED 看 request rate、errors、duration;USE 看 resource 的 utilization、saturation、errors。/ask API 可從 RED 起步,GPU queue 或資料庫 connection pool 則常需要 USE。

① The Question:dashboard 打開後,第一眼該看哪一塊?

Day 21 已經把 metrics、logs、traces 三種訊號都接起來了,但接起來不等於好用。事故發生時,值班工程師打開 Grafana,畫面上同時有十幾張圖:API latency、CPU 使用率、GPU queue 長度、5xx 比例……每張圖都合法,但沒有人事先講好「哪張圖先看」。問題不是資料不夠,是沒有把資料按「這張圖回答哪個問題」分類過。Golden Signals、RED、USE 存在的理由,就是先把「使用者受影響了嗎」「哪個 API 變慢」「資源是不是快撐不住」這三種不同層級的問題分開,而不是把三種數字混進同一張滿版 dashboard,逼 on-call 工程師用猜的。

「資料齊全」與「資料有用」是兩回事

Day 21 建好 Prometheus、Loki、Tempo 三件套後,很容易有一種錯覺:訊號都接上了,接下來只是「畫更多圖」的問題。但把三種訊號都接進 Grafana,跟「值班時能不能在兩分鐘內找到根因」完全是兩件事。一個常見的失敗模式是這樣的:

Day 21 完成後的第一版 dashboard
┌─────────────────────────────────────────────┐
│ API p50/p95/p99   CPU%   GPU%   Mem%         │
│ 5xx count         DB conn   Redis hit rate   │
│ request/sec       disk io   network in/out   │
│ container restart queue_depth  pod count     │
│ error log volume  trace error span count     │
└─────────────────────────────────────────────┘
        ↑
   14 張圖,一次性攤開在同一頁
   值班工程師:「所以我現在該看哪一個?」

這種 dashboard 不是沒有價值,它只是把「找出問題所在」的認知負擔,從系統設計者轉嫁給值班工程師——在凌晨三點、心跳還沒完全清醒、事故已經在燒 error budget 的當下,這個轉嫁成本特別高。SRE 圈子常說:「dashboard 不是用來證明你監控了很多東西,是用來讓值班工程師在最短時間內知道下一步做什麼。」Golden Signals、RED、USE 做的正是「先把問題分層、再決定哪張圖回答哪一層」的工作。

一個沒有框架的值班故事,長什麼樣子

用一個假設但貼近現實的時間軸,具體感受一下「資料齊全但沒有分層」在真實值班現場會拖慢多少時間:

02:14  客服反映使用者抱怨「AI 助理回答變超慢」
02:15  值班工程師打開 Grafana,畫面有 14 張圖
02:16  先看 CPU 使用率——45%,看起來正常,跳過
02:18  看 GPU 使用率——70%,猶豫這算不算高,
       Google 搜尋「GPU utilization 多少算飽和」
02:23  看 5xx count——沒有明顯增加,排除是明顯故障
02:26  看 request rate——正常,排除是流量暴增
02:30  想到要看看 queue,但發現沒有這張圖,
       開始在 Prometheus 裡臨時拼 query
02:38  拼出一條 queue depth 曲線,發現持續在升高
02:40  才開始往「worker 併發數」這個方向查
02:47  找到問題:上週的一次 deployment
       把併發數設定從 8 改成了 1
02:48  修正設定,問題解除
──────────────────────────
從收到回報到找到根因:34 分鐘

同一個場景,如果值班工程師手上有分層清楚的 dashboard 與決策表(第 ④ 節會給出這張表的具體樣子),流程會壓縮成:

02:14  客服反映使用者抱怨「AI 助理回答變超慢」
02:15  打開首頁:P95 TTFT 已經明顯偏離正常範圍
02:16  切到服務層 RED panel:/ask 的 5xx 沒有增加,
       但 duration P99 在惡化——排除是明顯故障
02:17  切到資源層 USE panel:queue depth 持續升高,
       GPU utilization 沒有明顯異常
02:18  對照 red_flag_action:「queue depth 升高,
       先查 worker concurrency 設定」
02:20  確認上週 deployment 把併發數改成了 1
02:21  修正設定,問題解除
──────────────────────────
從收到回報到找到根因:7 分鐘

兩段時間軸的差距,不是因為第二個值班工程師比較厲害,也不是因為系統本身的資料變多了——兩個場景裡的底層資料完全一樣。差距純粹來自於「資料有沒有被事先分層、排好優先順序、並附上第一步該做什麼」。這正是 Golden Signals、RED、USE 三套框架,加上第 ⑩ 節談的 dashboard 分層與 red_flag_action 規格,真正省下來的時間成本。

這篇文章要解決的,不是「該用哪個框架」,是「該怎麼組合」

網路上搜尋 Golden Signals vs RED vs USE,常常會得到「三選一」的印象——好像它們是互相競爭的方法論。這是常見誤解。Google SRE Book 提出 Golden Signals 時鎖定「監控一個服務最少該看哪四件事」;Tom Wilkie 提出 RED 時,明確說這是給微服務架構、request-driven 系統用的簡化版;Brendan Gregg 提出 USE 時鎖定硬體與作業系統資源層。三者作者群沒有互相對立的意思——RED 甚至可視為 Golden Signals 在 API 層的具體實作。真正該問的不是「這次事故該用 RED 還是 USE」,而是「我現在要回答的是使用者層、服務層,還是資源層的問題」。

② Traditional SRE:三套框架,三種讀者

在細看三套框架各自的內容之前,先用一張表把它們的出身講清楚,這對理解「為什麼三者不衝突」很有幫助——它們誕生的組織、要解決的問題規模、瞄準的讀者群,從一開始就不一樣:

Golden Signals RED USE
提出者 Google SRE 團隊 Tom Wilkie(Weaveworks/Grafana) Brendan Gregg
出處 Google SRE Book(2016) Weaveworks 工程部落格(2015 前後) 《Systems Performance》與個人部落格
要解決的規模 一個服務該監控什麼才算完整 大量微服務如何自動套用同一套 dashboard 模板 單一主機/單一資源如何快速定位瓶頸
瞄準的讀者 on-call SRE、服務owner 平台團隊、需要規模化監控的組織 效能工程師、資源層 troubleshooting
核心假設 使用者體驗可被四個維度概括 服務是 request-driven,可套模板 資源診斷可用三個問題窮舉

這張表最重要的一格是「要解決的規模」——Golden Signals 從「一個服務」出發,RED 從「一百個微服務」出發,USE 從「一顆 CPU/一張 GPU」出發。三者出發點不同,卻剛好對應到現代系統天然存在的三層粒度(使用者體驗、服務、資源),這也是為什麼疊起來用會比只挑一個更完整——它們原本就不是為了互相取代而設計的。

Golden Signals:使用者感受到的四件事

出自 Google SRE Book,適合服務整體健康度:

  • Latency:請求要多久才有回應
  • Traffic:現在有多少需求量
  • Errors:失敗比例
  • Saturation:系統距離滿載還有多少餘裕

Google SRE Book 在提出 Golden Signals 時,特別強調一個常被忽略的細節:latency 這一項不能只看「所有請求的平均延遲」,因為成功請求與失敗請求的延遲分佈往往完全不同——失敗請求可能因為快速失敗(fail-fast)而延遲極低,把它們和成功請求混在一起算平均,會讓真正拖慢使用者的延遲被平均掉。書中建議把「成功請求的延遲」和「失敗請求的延遲」分開追蹤,這個原則放到今天的 AI 系統依然成立:一個 timeout 在 30 秒後才失敗,跟一個 validation error 在 5 毫秒內就失敗,兩者的 latency 意義完全不同,不該被同一條曲線平均掉。

RED:API 層的三個問題

Tom Wilkie(Grafana、Weaveworks)提出的簡化版,適合單一服務或單一 endpoint:

  • Rate:每秒請求數
  • Errors:失敗請求數
  • Duration:請求耗時分佈

RED 的設計初衷值得多說一句:Tom Wilkie 在 Weaveworks 為微服務架構寫監控工具時,發現逐一為每個服務手動設計獨立的 dashboard 太耗工,於是抽出這三個「幾乎每個 request-driven 服務都該有」的維度,讓監控可以被自動化生成——只要一個服務用標準方式暴露 request count、error count、duration histogram,就能自動套用同一組 dashboard 模板和告警規則。這也是為什麼 RED 特別適合服務數量多、team 分散的組織:它犧牲了 Golden Signals 裡的 saturation(因為那通常需要對每個服務的資源特性有客製理解),換取「一套模板可以套用到幾十個微服務」的規模化能力。

USE:資源層的三個問題

Brendan Gregg 提出,適合 CPU、記憶體、磁碟、網路這類底層資源:

  • Utilization:資源被使用的比例
  • Saturation:有多少工作在排隊等這個資源
  • Errors:資源本身回報的錯誤事件數

USE 的起點跟 Golden Signals、RED 不太一樣——它不是從「監控一個服務」出發,而是從「系統效能診斷方法論」出發。Brendan Gregg 在自己的著作《Systems Performance》裡把 USE 定義為一套「checklist 方法」:對每一個硬體資源,依序問這三個問題,通常幾分鐘內就能找出瓶頸所在,而不用等到「靈光一閃猜到答案」。這也解釋了為什麼 USE 特別強調 Utilization 和 Saturation 要分開看——Gregg 在文章裡舉過一個經典例子:CPU utilization 100% 可能代表「CPU 忙著做有用的工作」,也可能代表「CPU 忙著自旋鎖等待,實際上什麼有用的事都沒做成」,只有把 saturation(run queue length)一起看,才能分辨這兩種完全不同的狀況。

三者不是互相取代,是分層對應

Golden Signals 回答「使用者現在好不好」,RED 回答「哪個 API 出問題」,USE 回答「哪個資源撐不住」。把這三層畫在同一張圖,等於同時問三個問題,答案通常會互相干擾。

用一個簡化的架構圖表示這個分層關係:

┌───────────────────────────────────────────────┐
│  使用者層 — Golden Signals                      │
│  「使用者現在好不好?」                           │
│  latency / traffic / errors / saturation       │
└──────────────────────┬──────────────────────────┘
                        │ 往下拆解:是哪個 API
┌──────────────────────▼──────────────────────────┐
│  服務層 — RED                                    │
│  「哪個 API/哪個 endpoint 出問題?」              │
│  rate / errors / duration                       │
└──────────────────────┬──────────────────────────┘
                        │ 往下拆解:是哪個資源
┌──────────────────────▼──────────────────────────┐
│  資源層 — USE                                    │
│  「哪個資源撐不住?」                             │
│  utilization / saturation / errors              │
└───────────────────────────────────────────────┘

這張圖的箭頭方向很重要:正常的除錯流程是由上往下(使用者受影響 → 找出哪個服務 → 找出哪個資源),而不是反過來從資源指標開始猜使用者受不受影響。看到 GPU utilization 飆到 95%,第一反應不該是「使用者一定受影響了」,而該先往上確認 Golden Signals 是否真的變差;如果使用者體感正常,這張 GPU 圖此刻只是「資源被有效利用」的證據,不是故障訊號。

一個容易混淆的對照:RED 的 Errors ≠ USE 的 Errors

三套框架共用幾個相似的詞彙,這是它們最容易被混著用錯的地方。最典型的一組是「Errors」——RED 和 USE 都有 Errors 這個維度,但兩者量的完全不是同一件事:

RED 的 Errors
是使用者能感受到的失敗
+
「這次請求,使用者拿到的是錯誤還是正確的回應?」
(分子分母都以「請求」為單位)

USE 的 Errors
是資源本身回報的錯誤事件
+
「這個資源,在提供服務的過程中出了什麼差錯?」
(分子分母都以「資源事件」為單位,不對應到單一使用者請求)

一個具體的例子最能說明這個差異:GPU 發生一次 ECC 記憶體錯誤並自動修正,這是一個 USE 的 Errors 事件(資源層出了差錯),但如果這次修正沒有造成任何請求失敗,RED 的 Errors 完全不會動;反過來,一次因為 prompt 格式錯誤造成的 validation_failed,是一個明確的 RED Errors 事件(使用者沒拿到能用的答案),但這次失敗完全不涉及任何底層資源異常,USE 的 Errors 也不會動。把這兩種 Errors 疊在同一條曲線上看,會讓「使用者受影響的失敗」和「資源健康度事件」互相稀釋,變成一條誰都解讀不出意義的合成指標。

常見誤解:把三套框架當成三張獨立的 dashboard 分頁

一個很容易犯的錯,是把 Golden Signals、RED、USE 理解成「三個互不相干的監控主題」,各自建一個 Grafana folder,彼此沒有連結。這樣做的問題是:使用者層看到 latency 升高時,沒有辦法一鍵跳到「是哪個 API」;服務層看到某個 endpoint duration 變差時,沒有辦法一鍵跳到「是哪個資源」。三套框架真正該被實作成的樣子,是同一份 root cause 分析路徑上的三個檢查點,而不是三本互不引用的參考書。這也是第 ⑩ 段要談的「dashboard 分層」設計的核心動機。

③ AI Era Extension:多一層 workflow,也多一層資源

RED 用在 /ask 這類 API 沒問題,但 AI workflow 的耗時由 retrieval、model call、tool call 等多個步驟組成。只看整體 duration,無法知道變慢的是 retriever、model 還是 tool;把 RED 拆到各個子步驟,才能定位瓶頸。

舉個具體數字:假設一次 /ask 的 end-to-end duration 是 3.2 秒,只看這一個數字,工程師大概只能猜「模型太慢」,動手加 GPU 或換更快的 model。但把同一次請求拆成子步驟,可能長這樣:

retrieval   0.4s
queue_wait  1.8s   ← 真正的大頭
model       0.9s
tool        0.1s
------------------
total       3.2s

真正吃掉時間的是 queue_wait,也就是請求排隊等 worker 的時間,跟 model 本身算得快不快無關。如果只盯著總 duration 這一條 RED 曲線,這 1.8 秒會被誤算進「model 變慢」,接下來的動作(換模型、升級硬體)全部打歪;只有把 RED 拆到子步驟層級,才能讓「哪一段」變成可回答的問題,而不是靠猜。

為什麼傳統 RED 對 AI workflow 不夠用

傳統 web API 的 RED 夠用,是因為一次請求通常只經過「應用程式碼 → 資料庫」這種相對淺的呼叫鏈,duration 變異來源有限。AI workflow 不是這個形狀。一次 /ask 呼叫可能包含:

一次 /ask 請求可能經過的階段
┌──────────────┐
│ 1. 輸入驗證    │  duration 通常穩定、毫秒級
└──────┬───────┘
       ▼
┌──────────────┐
│ 2. Retrieval  │  duration 依向量資料庫負載、index 大小浮動
└──────┬───────┘
       ▼
┌──────────────┐
│ 3. Queue wait │  duration 依 worker 併發數、當下流量劇烈浮動
└──────┬───────┘
       ▼
┌──────────────┐
│ 4. Model call │  duration 依 prompt 長度、model route、TTFT/生成 token 數浮動
└──────┬───────┘
       ▼
┌──────────────┐
│ 5. Tool call  │  duration 依外部 API、可能重試、可能逾時
└──────┬───────┘
       ▼
┌──────────────┐
│ 6. Validator  │  duration 通常穩定,但失敗會觸發重試整段流程
└──────────────┘

每一段的 duration 分佈型態都不一樣:輸入驗證接近常數,retrieval 隨資料庫負載波動,queue wait 在尖峰時可能指數成長,model call 隨 prompt 與輸出長度線性增加,tool call 有網路不確定性。把這六段疊在一起只看一條 end-to-end duration 曲線,等於用一個數字描述六種不同隨機變數之和——統計上可以做(卷積),但診斷上毫無意義:P95 變差時,你不知道是哪一段的分佈右移了。

這正是為什麼 Day 20(Tempo 分散式追蹤)建立的 span 邊界,在這裡變成必要條件:沒有 trace 只能拿到一個總數;有了 trace,才能對每個 span 分別套用 duration 維度,變成「六個 mini-RED」而不是一個籠統的 RED。

USE 延伸到 GPU:從抽象方法論到具體指標

USE 這層則直接延伸到 GPU:GPU utilization、GPU memory saturation、inference queue 長度,都是 USE 方法可以直接套用的資源指標,只是資源從 CPU/磁碟換成 GPU/VRAM/推論佇列。

把 CPU 時代的 USE 對照表換成 GPU 版本,會長這樣:

                CPU(傳統 USE)           GPU(AI Era USE)
Utilization     CPU busy % (mpstat)      GPU compute utilization(SM busy %)
Saturation      run queue length         inference queue depth、batch 等待、VRAM allocation pressure
Errors          machine check exception  OOM、driver reset、Xid error、ECC error、worker crash

這張對照表看起來簡單,但魔鬼藏在細節裡。GPU utilization 在不同 exporter、不同推論框架下,代表的意義都不完全相同:有些指標量的是「SM 在取樣時刻是否有任何 kernel 在執行」,這種定義下即使 GPU 大部分時間只做低效運算,utilization 仍可能顯示接近 100%——這跟 CPU 領域「utilization 高但花在自旋鎖」是同一種結構性陷阱,只是換了硬體。這也是為什麼第一步永遠是先看 exporter 本身暴露的原始指標,而不是假設一個熟悉的名稱就直接拿來用:

# 先看你的 GPU exporter 實際暴露了什麼,
# 而不是假設它跟別人的教學文章用一樣的指標名稱
curl -s http://gpu-exporter:9400/metrics | grep -i "gpu\|dcgm" | head -20

# 常見會出現、但定義因 exporter 版本而異的幾類指標:
# DCGM_FI_DEV_GPU_UTIL         — SM 忙碌比例(取樣式,非精確工作量)
# DCGM_FI_DEV_FB_USED          — VRAM 已用量(saturation 訊號的一部分)
# DCGM_FI_DEV_FB_FREE          — VRAM 剩餘量
# DCGM_FI_DEV_XID_ERRORS       — driver/硬體層錯誤事件(USE 的 Errors)

即使是這裡列出的例子,也只是 NVIDIA DCGM exporter 這一種選擇下常見的欄位名稱,換一套推論框架可能用完全不同的命名慣例。把這串指令養成習慣,比背下任何一組「看起來通用」的 GPU metric 名稱都可靠。

傳統 RED/USE 完全沒涵蓋的訊號

除了把既有框架套進新元件,AI workflow 還多出一批傳統三套框架都沒涵蓋的訊號:queue delay(請求排隊等 GPU 資源的時間)、TTFT(time to first token,使用者等到第一個字出現的時間)、tool failure(工具呼叫失敗但整體請求仍回傳 200)、evaluation outcome(Day 08 建立的 quality/safety 判定結果)。它們把 Golden Signals 的「使用者感受」延伸到 AI workflow 特有的等待與失敗模式;最後仍要回答「這件事有沒有影響使用者拿到正確結果」。

值得展開的是這四個訊號各自屬於哪一層、又為什麼傳統框架接不住它們:

Queue delay 介於 RED 和 USE 之間——它是一個 per-request 的等待時間(符合 RED 的 duration 定義),但它的根因幾乎永遠在資源層(worker 併發數不夠、GPU 被佔滿)。傳統 web 服務通常沒有這麼明顯的排隊現象,因為大多數 API 呼叫是無狀態、可以水平擴展到近乎瞬間完成;但 GPU 推論是一種「昂貴、有限並行度」的資源,請求排隊是常態而非例外,需要被明確地量測出來,而不是被吞進 end-to-end duration 裡。

TTFT 是串流輸出時代特有的訊號。傳統 RED 的 duration 假設「請求完成」是一個單一時刻的事件;但串流回應把「完成」拆成了「開始輸出」和「全部輸出完畢」兩個不同時刻,使用者對這兩個時刻的感受天差地遠。這一點在下篇(下)會用具體的使用者心理研究數字展開。

Tool failure 挑戰的是 RED 裡「Errors」的定義。傳統服務的 error 幾乎等同於「HTTP 5xx 或例外」,但 AI workflow 常見的模式是:某個工具呼叫失敗了,系統選擇降級(回傳一個較弱的答案、或跳過這個工具),最終仍以 HTTP 200 回應使用者。如果 RED 的 Errors 維度只數 5xx,這類降級會完全消失在監控裡——這正是 Day 07 討論過的 technical success 與 task success 的分裂,在 RED 這個框架裡的具體投影。

Evaluation outcome 則完全超出 RED/USE 的設計範疇。RED 和 USE 兩者都是為了回答「系統有沒有正常運作」而設計的,但 AI workflow 額外多了一個問題:「系統正常運作,答案卻是錯的」。這需要獨立於 RED/USE 之外的第三層訊號,下篇(下)會用兩個真實案例(Anthropic、silent failure 研究)具體說明為什麼這一層不能被省略。

④ 先選問題,再選圖:一個 dashboard 的最小決策表

框架很容易被背成縮寫,然後被用成儀表板裝飾。真正有用的作法反過來:先寫事故現場想回答的問題,再決定哪個框架和哪個訊號負責回答。

下面這張表刻意不追求「所有指標都列到」。它只保留值班時可以改變下一步動作的欄位。

事故現場的問題 第一個看哪個框架 第一張圖 正常卻仍可能有問題的地方 下一步
使用者現在是否明顯受影響? Golden Signals successful task ratio、P95 end-to-end latency HTTP 成功率高,回答仍可能錯 看 evaluation outcome 與抽樣 trace
/ask 是否正在變慢或失敗? RED rate、5xx/timeout ratio、P95 duration 平均延遲正常,P99 已經壞掉 依 endpoint、model route、deployment 切分
為何只有部分請求很慢? RED + trace retrieval、model、tool 的 duration 服務總耗時看不出子步驟 從慢 trace 找共同 span
GPU worker 還能接多少流量? USE utilization、queue depth、queue wait GPU utilization 低也可能卡在 CPU 或 mutex 看 worker concurrency、CPU、排隊時間
資料庫連線是否耗盡? USE pool in-use、waiters、connection errors CPU 很低,連線池仍可能全滿 查 pool 上限與慢查詢
某次 rollout 是否傷到品質? Golden Signals + evaluation versioned task success、burn rate 技術錯誤沒有增加 對照 prompt/model/retrieval version

這張表也揭露一件不太討喜的事:一個框架不會自動給答案。GPU utilization=92% 本身不是故障;如果 queue wait 沒增加、TTFT 還在目標內,它可能只是資源被有效使用。反過來說,GPU utilization=35% 也不保證健康,worker 有可能被單一工具呼叫或 connection pool 卡住。

因此數字必須和「使用者結果」相連。每張 operational panel 都要能往上連到 Golden Signals;每個 user-impact panel 都要能往下連到 RED、USE 和 trace。斷掉任何一段,dashboard 都只是在陳列數字。

決策表怎麼用:從「症狀」出發,不是從「我有這張圖」出發

這張表容易被誤用成「查表工具」——事故發生時對照表格找對應的圖。但真正該被使用的方式是反過來:先讓值班工程師(或 on-call runbook)寫出「現在觀察到的症狀是什麼」,再讓症狀決定往下查哪一層。這跟很多團隊「先打開所有 dashboard 一張一張看過去」的習慣相反——資訊量越大,人在高壓情境下決策反而越慢(選擇越多決策時間越長,是認知心理學已反覆驗證的現象)。決策表的價值正是事先把「症狀 → 該看哪張圖」想清楚,值班時不需要臨場重新推理。

表格背後藏著一個更深的問題:「正常卻仍可能有問題的地方」欄位是整張表最重要的一欄

仔細看這張表的第四欄,會發現它其實在講同一件事的六種變體:任何單一指標「看起來正常」都不足以下結論,因為每個指標都只回答了它被設計要回答的那一個問題,而不是全部的問題。這句話值得拆開來看每一列:

  • HTTP 成功率高,不代表回答正確——因為 HTTP 成功率量的是「技術流程有沒有跑完」,不是「跑完的結果對不對」。
  • 平均延遲正常,不代表沒有一群使用者在受苦——因為平均值會被大量快速請求稀釋掉少數極慢請求的存在。
  • 服務總耗時正常,不代表內部沒有互相抵銷的異常——例如 retrieval 變慢 1 秒、但 model 因為快取命中變快 1 秒,總和看起來毫無變化。
  • GPU utilization 低,不代表還有真正可用的容量——因為 utilization 只量「GPU 在忙」,不量「GPU 在忙什麼」,也不量「還有什麼東西在排隊等別的資源」。
  • CPU 很低,不代表連線沒有耗盡——因為連線池是一種完全獨立於 CPU 的資源,被鎖住的連線不會讓 CPU 忙碌。
  • 技術錯誤沒有增加,不代表這次上線是安全的——因為語意層的劣化(下篇(下)會深入談)不會反映在任何技術錯誤指標上。

這六個例子有一個共同的結構:每一個「正常」的指標背後,都藏著一個沒有被這個指標涵蓋的維度。這正是「多套框架搭配使用」存在的意義——不是因為某一套框架設計得不好,而是因為任何單一指標,本質上都只能回答一個切面的問題。

同一個症狀,三套框架會問出不同問題

假設客服 RAG 服務在 10:05 開始收到「回答出現得很慢」的回報。此時不需要先猜是模型、GPU 或網路。

10:05  使用者感受:串流第一個 token 出現變慢
10:06  Golden Signals:P95 TTFT 上升;task success 暫時未知
10:07  RED:/ask rate 穩定,5xx 沒升,duration P99 上升
10:08  USE:GPU utilization 70%,inference queue depth 從 2 升到 38
10:09  Trace:model span 前面多了 queue_wait span;retrieval 正常

這不是「RED 沒用」。RED 正確地說出 API 很慢但沒有大量錯誤;USE 說明排隊正在形成;trace 指向哪一段在等。三者合起來,才足以支持「先限制新流量、增加 worker 或調整 admission control」這類動作。

若只看 5xx,這起事故會被判成一切正常。若只看 GPU utilization,工程師可能錯誤地加 GPU;其實 queue 的根因可能是 worker concurrency 被設成 1。若只看單一 trace,又會不知道這是偶發請求還是所有使用者都被影響。

把這五分鐘拆開看,每一分鐘都在排除一個錯誤假設,而不是單純疊加資料:

  • 第 1 分鐘只有客訴,任何猜測都只是直覺,不是證據。
  • 第 2 分鐘 Golden Signals 把體感翻成 P95 TTFT,同時誠實標出「task success 暫時未知」——延遲類問題能立刻回答,答案對不對要等 evaluation 跑完。
  • 第 3 分鐘 RED 排除了兩個最直覺卻錯誤的假設(流量暴增、服務掛了),把注意力收斂到「請求還在跑,只是尾端變慢」。
  • 第 4 分鐘 USE 給出真正的診斷訊號:GPU utilization 70% 本身模稜兩可,但 inference queue depth 從 2 陡升到 38,這個變化量才是 Saturation 在說的事——工作正在累積、來不及消化。
  • 第 5 分鐘 trace 補上最後一塊拼圖:不是 model 本身變慢,是 model span 前面多了 queue_wait,這個區別直接決定下一步該查 model provider 還是 worker 併發數。

五分鐘、四層訊號合起來才拼出完整根因鏈:使用者體感變慢 → API 尾端延遲惡化但技術沒壞 → inference queue 正在累積 → 排隊發生在進 model 之前。任何一層單獨拿出來都撐不住「調整 worker 併發數」這個修復動作——這正是本文開頭那句話在一次具體事故裡的完整展開。

⑤ 把框架落在指標契約,而不是 Grafana 版面

在建 dashboard 前,先寫 metric contract。這一步很像 Day 23 要寫的 trace schema:名稱、單位、label、分母與可回答的問題都要先講清楚。Grafana 可以換,這份契約不該跟著畫面一起消失。

以下是可用於 Lab 的最小 contract。指標名稱只是範例;重點在每個欄位的邊界。

指標 類型 單位 低基數 labels 回答的問題
ai_requests_total Counter requests route、outcome、model_route RED rate 與結果比例
ai_request_duration_seconds Histogram seconds route、outcome API duration 與 tail latency
ai_ttft_seconds Histogram seconds route、model_route 使用者等第一個 token 多久
ai_queue_wait_seconds Histogram seconds queue、model_route 請求在 worker 前等多久
ai_workflow_steps_total Counter steps step、outcome retrieval/tool/parser 哪步失敗
ai_evaluations_total Counter evaluations result、evaluator_version quality/safety 的樣本結果
ai_inference_queue_depth Gauge requests queue 當下排隊壓力

不要把 request_id、user_id、完整 prompt、文件內容、完整錯誤訊息放進 Prometheus label。它們的基數與敏感度都不適合 time series;Day 21 已經把這些細節留給 structured log 和 trace。可接受的 label 應該是一組有限、預先定義的值,例如 outcome="ok" | "upstream_timeout" | "validation_failed"。

model_route 也要有限集合,例如 primary、fallback、local。不要直接以動態 model ID、tenant ID 或 prompt version 當 label。版本比較可放在 deployment annotation、log/trace attribute,或在有明確保留策略的 evaluation 資料集中處理。

為什麼要先寫契約,才動手建 dashboard

「先寫契約,再建 dashboard」聽起來像流程官僚,但解決的是實際痛點:dashboard 是視覺產物,重建成本低;指標契約是資料產物,一旦開始累積歷史資料,改名稱、改 label 結構、改 bucket 邊界,都會讓舊資料和新資料變得不可比較,甚至歷史查詢直接壞掉——這跟資料庫 schema migration 是同一類問題,UI 可以隨時重畫,但底層 schema 一旦定型,改動成本會隨資料量指數成長。

指標契約要回答的問題比表面上看到的多。以 ai_request_duration_seconds 這一列為例,一份完整的契約至少要講清楚:

指標名稱: ai_request_duration_seconds
類型: Histogram
單位: seconds(Prometheus 官方慣例——base unit,不要用 milliseconds)
Label: route, outcome
Label 基數上限: route 固定集合(目前僅 /ask);outcome 固定 7 種取值
Bucket 邊界: 0.1, 0.3, 0.5, 1, 2, 5, 10, 30(依 SLO 目標校準,不是隨手選的等比數列)
分母定義: 每次 /ask 請求完成(成功或失敗)都會被 observe 一次;被 client 中斷連線但 server 端仍在處理的請求,仍會在 server 端完成時被記錄
不會做的事: 不記錄逐一 request 的原始耗時(那是 trace 的責任);不記錄 retry 前的耗時(重試視為新的一次 observe,避免重複計數扭曲分佈)
擁有者: platform-observability team

這份契約裡「不會做的事」跟「retry 視為新的一次 observe」這兩句特別容易被忽略,但正是造成 dashboard 數字「看起來合理但實際上錯」最常見的來源之一——下篇(下)會再談這個「retry 被重複計數」的陷阱。

把契約寫成可以被版控、被 review 的檔案,而不是只存在腦中

「先寫契約」這句話如果沒有落成具體檔案,很容易變成一句沒人真的做的口號。實務上一個可行的做法,是把整份 metric contract 寫成一份 YAML,跟程式碼一起進版控,任何新增或修改指標都要走一次 PR review,而不是工程師覺得需要就自己加一個新 label:

# observability/metric-contract.yaml
metrics:
  - name: ai_requests_total
    type: counter
    unit: requests
    labels:
      route:
        cardinality: fixed
        values: ["/ask"]
      outcome:
        cardinality: fixed
        values:
          - ok
          - technical_error
          - upstream_timeout
          - tool_failed
          - validation_failed
          - quality_failed
          - safety_refused
      model_route:
        cardinality: fixed
        values: ["primary", "fallback", "local"]
    answers: "RED rate 與結果比例"
    owner: platform-observability

  - name: ai_request_duration_seconds
    type: histogram
    unit: seconds
    buckets: [0.1, 0.3, 0.5, 1, 2, 5, 10, 30]
    labels:
      route: { cardinality: fixed, values: ["/ask"] }
      outcome: { cardinality: fixed, values: ["<同上七種>"] }
    answers: "API duration 與 tail latency"
    owner: platform-observability

  - name: ai_inference_queue_depth
    type: gauge
    unit: requests
    labels:
      queue: { cardinality: fixed, values: ["inference"] }
    answers: "當下排隊壓力"
    caveat: "僅代表單一 process 內計數,多 worker 時不可直接相加宣稱全域 depth"
    owner: platform-infra

把契約寫成這種結構化檔案帶來兩個好處:cardinality: fixed 加明確的 values 列表,可以搭配 CI 檢查腳本在 PR 階段就擋下不合規的 label 值,把規則從「code review 靠人記得」變成自動化;owner 欄位則直接呼應第 ⑩ 節「每個 panel 都應有 owner」——指標一誕生就標記負責團隊,之後追問「這個指標為什麼長這樣」不用考古。

反過來說,沒有契約檔案的常見退化路徑是:第一個工程師加了 route、outcome;三個月後另一個工程師為排查特定客戶問題臨時加了 customer_id;六個月後又加了 prompt_version。每次新增當下都顯得合理,但沒有契約強制 review,cardinality 就這樣一步步失控——本系列另一篇文章記錄過真實案例:開發者為追蹤使用者流量加上 user_id label,三個月內在百萬活躍使用者規模下產生超過 500 萬個時間序列,讓 Prometheus OOM killed、Grafana 全部空白,恢復動用了戰情室與程式碼回滾。

先定義 outcome,才有資格算 error ratio

AI 系統常見的錯誤,是把 HTTP 200 全都算進成功分子。這會讓技術成功蓋住 workflow 失敗和語意失敗。

在這個系列裡,可以先用下列 outcome 契約:

ok                  技術流程完成,且已通過必要 validator
technical_error     API 或內部程式無法完成流程
upstream_timeout    模型或工具依賴逾時
tool_failed         工具失敗,服務回覆降級結果或錯誤
validation_failed   回應格式、引用或規則檢查未通過
quality_failed      evaluator 判定不符合任務要求
safety_refused      系統依政策拒答;是否算 good outcome 取決於任務

這不是通用分類法。真正的產品要和產品、風險與客服團隊定義「拒答在什麼任務中屬於正確結果」。例如使用者要求危險操作時,safety_refused 應該算 policy success;使用者詢問正常的公司制度卻被錯誤拒答,則不能把它包裝成成功。

outcome 分類法要能承受「誰來標」這個問題

上面這七種 outcome,寫在 code 裡看起來乾淨俐落,但每一種在實際運作時都有一個「誰決定、什麼時候決定」的問題。technical_error 與 upstream_timeout 最容易正確標記,幾乎完全由程式邏輯決定,可以寫單元測試驗證(下篇(下)會提到)。tool_failed 和 validation_failed 需要工程團隊明確定義「什麼叫工具失敗但仍要降級回應」,決策依據該來自產品對使用者體驗的容忍度,不是工程師覺得方便就好。quality_failed 和 safety_refused 最麻煩,依賴 Day 08 建立的 evaluator,判準會隨版本、prompt、抽樣策略改變——這也是為什麼 ai_evaluations_total 要帶 evaluator_version label,否則升級前後的 quality failure ratio 根本不可比較,卻會被誤讀成品質變好或變差。

一個實用的做法是把這七種 outcome 按照「誰能標記、標記時間點」畫成一張責任表:

outcome              誰標記            標記時間點
ok                   程式邏輯           請求完成當下
technical_error      程式邏輯(例外類別) 請求完成當下
upstream_timeout     程式邏輯(timeout) 請求完成當下
tool_failed          程式邏輯 + 工具回傳碼 請求完成當下
validation_failed    validator 規則      請求完成當下
quality_failed       evaluator(抽樣)    請求完成後的非同步評估
safety_refused       policy engine       請求完成當下,但「算不算 good」需要人工覆核政策

前五種都是「請求完成當下就能決定」的同步 outcome;後兩種要嘛需要非同步評估(quality_failed 常常要等 evaluator 跑完才知道),要嘛需要政策判斷(safety_refused 是否算 good outcome,本質上不是工程問題)。把這張責任表寫清楚,可以避免一個常見的組織性錯誤:工程團隊自己決定「safety_refused 一律算失敗」或「一律算成功」,卻沒有讓風控、法務或產品參與這個判斷。

用測試把 outcome 映射鎖住,而不是靠人工複查

下篇(下)會提到「outcome 映射不一致」是實務上最容易讓 dashboard 產生合理但錯誤數字的原因之一,這裡先把對應的修法寫出來,因為它跟這份 outcome 契約是同一件事的兩面:契約定義了「應該有哪七種 outcome、各自的判準是什麼」,測試則負責確保「程式碼實際產生的 outcome,真的符合契約寫的規則」。一份最小可用的測試,長得像這樣:

import pytest

from app.outcome import map_exception_to_outcome


@pytest.mark.parametrize(
    "exc, expected_outcome",
    [
        (TimeoutError("upstream took too long"), "upstream_timeout"),
        (ToolInvocationError("search tool returned 500"), "tool_failed"),
        (ResponseValidationError("missing citation"), "validation_failed"),
        (RuntimeError("unexpected null pointer"), "technical_error"),
    ],
)
def test_exception_maps_to_expected_outcome(exc, expected_outcome):
    assert map_exception_to_outcome(exc) == expected_outcome


def test_every_outcome_value_is_in_the_contract():
    # 防止有人在程式碼裡手打一個契約表格外的字串,
    # 例如打錯成 "upstream_time_out"(底線位置錯誤)
    # 這種錯字不會讓程式壞掉,但會在 Prometheus 裡
    # 悄悄多出一個新的 label 組合,永遠不會被
    # 既有的 PromQL query(例如 outcome=~"...|upstream_timeout|...")匹配到。
    from app.outcome import KNOWN_OUTCOMES

    assert KNOWN_OUTCOMES == {
        "ok",
        "technical_error",
        "upstream_timeout",
        "tool_failed",
        "validation_failed",
        "quality_failed",
        "safety_refused",
    }

第二個測試函式在防的是:如果某處手打了 "upstream_time_out"(底線位置打錯),Python 不會報錯,Prometheus 也會乖乖收下這個新 label 值——但這個錯字永遠不會被下篇那條 PromQL 正則式匹配到,於是這類失敗會從 error ratio 的分子裡消失,錯誤率顯示得比真實情況低,而且低得毫無徵兆。用固定的 KNOWN_OUTCOMES 集合把契約寫進程式碼、並用測試鎖住它,能把這種靜默偏移在 CI 階段就攔下來,而不是等值班工程師發現「error ratio 跟客訴量對不上」才開始懷疑指標本身有問題。

下集預告:下篇(下)會把今天定義的指標契約接上實際的 FastAPI 實作與 Prometheus panel,並用兩個可重跑的事故情境驗證框架選得對不對,最後談框架的限制與 dashboard 分層原則。


這篇是 Learning SRE for the AI Era 系列的一部分。

我會從 SRE 的服務可靠性基礎開始,逐步探索當系統加入 LLM、RAG、Agent 與 GPU Infrastructure 後,如何讓 AI 系統不只可用,也能被觀測、評估、控制成本並安全演進。

Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 21(下)|Metrics、Logs、Traces:同一件事,三種問題
下一篇
Day 22(下)|Golden Signals、RED、USE:框架是起點,不是儀表板模板
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 44 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言